#include "webserver.h"


/*
端口号，服务器的事件触发模式， 超时时间， 控制套接字关闭时的行为
数据库服务器监听的端口号， 用户名， 用户密码
数据库名称， 连接池数量， 线程池线程数量
是否开启日志， 日志等级，日志异步队列容量
*/
WebServer::WebServer(int port, int trigMode, int timeOutMs, bool optLinger,
                     int sqlPort, const char* sqlUser, const char* sqlPwd,
                     const char* dbName, int connPoolNum, int threadNum,
                     bool openLog, int logLevel, int logQueSize)
    : port_(port)
    , openLinger_(optLinger)
    , timeOutMs_(timeOutMs)
    , isClose_(false)
    , timer_(std::make_unique<HeapTimer>())
    , threadPool_(std::make_unique<ThreadPool>(threadNum))
    , epoller_(std::make_unique<Epoller>())
{
    // 用于获取当前工作目录的绝对路径
    srcDir_ = getcwd(nullptr, 256);
    assert(srcDir_);
    strncat(srcDir_, "/resources", 16);
    // 初始化用户数量和资源路径
    HttpConn::userCount = 0;
    HttpConn::srcDir = srcDir_;
    // 初始化数据库连接池，"localhost",通常解析为本地计算机的 IP 地址
    SqlConnPool::instance()->init("localhost", sqlPort, sqlUser, sqlPwd, dbName, connPoolNum);

    // 配置监听套接字和客户端连接套接字的事件触发模式以及相关事件标志
    initEventMode(trigMode);
    if(!initSocket()) {
        isClose_ = true;
    }

    // 开启日志系统
    if(openLog) {
        Log::instance()->init(logLevel, "./log", "log", logQueSize);
        if(isClose_) {
            LOG_ERROR("========== Server init error!==========");
        } else {
            LOG_INFO("========== Server init ==========");
            LOG_INFO("Port : %d, OpenLinger : %s", port_, openLinger_ ? "true" : "false");
            LOG_INFO("Listen Mode : %s, OpenConn Mode : %s", 
                        (listenEvent_ & EPOLLET ? "ET" : "LT"),
                        (connEvent_ & EPOLLET ? "ET" : "LT"));
            LOG_INFO("Log System Level : %d", logLevel);
            LOG_INFO("Resources dirent : %s", HttpConn::srcDir);
            LOG_INFO("SqlConnPool number : %d, ThreadPool number : %d", connPoolNum, threadNum);
        }
    }
}

WebServer::~WebServer()
{
    close(listenFd_);
    isClose_ = true;
    // getcwd会分配内存，需要手动进行释放
    free(srcDir_);
    SqlConnPool::instance()->closePool();
}

// 设置监听套接字和客户端连接套接字的事件触发模式
void WebServer::initEventMode(int trigMode)
{
    // EPOLLRDHUP 同样用于检测客户端关闭连接或者半关闭连接的情况
    // EPOLLONESHOT 保证一个事件在被处理期间，同一个文件描述符上不会再次触发相同的事件
    listenEvent_ = EPOLLRDHUP;
    connEvent_ = EPOLLONESHOT | EPOLLRDHUP;
    switch (trigMode)
    {
    case 0:
        break;
    case 1:
        // 客户端连接套接字使用边缘触发模式，监听套接字使用默认触发模式
        connEvent_ |= EPOLLET;
        break;
    case 2:
        // 监听套接字使用边缘触发模式，而客户端连接套接字使用默认触发模式
        listenEvent_ |= EPOLLET;
        break;
    case 3:
        // 监听套接字和客户端连接套接字都使用边缘触发模式
        listenEvent_ |= EPOLLET;
        connEvent_ |= EPOLLET;
        break;
    default:
        // 监听套接字和客户端连接套接字都使用边缘触发模式
        listenEvent_ |= EPOLLET;
        connEvent_ |= EPOLLET;
        break;
    }
    // 记录客户端连接套接字的触发模式
    HttpConn::isET = (connEvent_ & EPOLLET);
}

// 开启服务器的事件循环，不断监听和处理来自客户端的连接和数据读写事件
void WebServer::start()
{
    int timeMs = -1;
    if(!isClose_) {
        LOG_INFO("========== Server start ==========");
    }
    //  开启事件循环
    while(!isClose_) {
        // 获取下一个定时器触发时间
        if(timeOutMs_ > 0) {
            timeMs = timer_->getNextTick();
        }

        // 等待事件的发生并且处理发生的事件
        int eventCnt = epoller_->wait(timeMs);
        // 循环遍历所有发生事件的文件描述符
        for(int i = 0; i < eventCnt; ++i) {
            int fd = epoller_->getEventFd(i);
            uint32_t events = epoller_->getEvents(i);
            // 监听套接字事件
            if(fd == listenFd_) {
                dealListen();
            } else if(events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {
                // 如果是对端关闭连接或半关闭连接或者挂起事件和错误事件，则关闭连接
                assert(user_.count(fd) > 0);
                closeConn(&user_[fd]);
            } else if(events & EPOLLIN) {
                // 可读事件，读取客户端发送的数据，并进行相应的解析和处理
                assert(user_.count(fd) > 0);
                dealRead(&user_[fd]);
            } else if(events & EPOLLOUT) {
                // 可写事件，将服务器要发送的数据写入客户端连接的写缓冲区
                assert(user_.count(fd) > 0);
                dealWrite(&user_[fd]);
            } else {
                // 发生了意外事件
                LOG_ERROR("Unexpected event");
            }
        }
    }
}

// 在服务器遇到错误情况时，向客户端发送错误信息并关闭连接
void WebServer::sendError(int fd, const char* info)
{
    assert(fd > 0);
    int ret = send(fd, info, strlen(info), 0);
    if(ret < 0) {
        LOG_WARN("send error to client[%d] error!", fd);
    }
    close(fd);
}

// 关闭客户端的连接
void WebServer::closeConn(HttpConn* client)
{
    assert(client);
    LOG_INFO("Client[%d] quit!", client->getFd());
    epoller_->delFd(client->getFd());
    client->close();
}

// 添加客户端的连接（在调用之前客户端就已经与服务器建立了连接）
void WebServer::addClient(int fd, sockaddr_in addr)
{
    assert(fd > 0);
    user_[fd].init(fd, addr);
    // 添加定时器
    if(timeOutMs_ > 0) {
        timer_->addNode(fd, timeOutMs_, std::bind(&WebServer::closeConn, this, &user_[fd]));
    }
    // 监听客户端的可读事件并且设置为非阻塞模式
    epoller_->addFd(fd, EPOLLIN | connEvent_);
    setFdNonBlock(fd);
    LOG_INFO("Client[%d] in!", user_[fd].getFd());
}

// 对新连接的客户端进行相应的处理
void WebServer::dealListen()
{
    struct sockaddr_in addr;
    socklen_t len = sizeof(addr);
    do {
        int fd = accept(listenFd_, (struct sockaddr*)&addr, &len);
        if(fd < 0) {
            return;
        } else if(HttpConn::userCount >= MAX_FD) {
            // MAX_FD 服务器允许的最大客户端连接数量
            sendError(fd, "Server busy!");
            LOG_WARN("Client is full!");
            return;
        }
        addClient(fd, addr);
    // 监听套接字使用边缘触发模式， 需要在一次处理中尽可能多地接受新连接
    } while(listenEvent_ & EPOLLET);
}

// 处理客户端读事件
void WebServer::dealRead(HttpConn* client)
{
    assert(client);
    // 延长客户端连接的超时时间
    extentTime(client);
    threadPool_->addTask(std::bind(&WebServer::onRead, this, client));
}

// 处理客户端写事件
void WebServer::dealWrite(HttpConn* client)
{
    assert(client);
    extentTime(client);
    threadPool_->addTask(std::bind(&WebServer::onWrite, this, client));
}

// 延长客户端连接的超时时间
void WebServer::extentTime(HttpConn* client)
{
    assert(client);
    if(timeOutMs_ > 0) {
        timer_->adjust(client->getFd(), timeOutMs_);
    } 
}

// 根据客户端 HTTP 请求的处理结果，
// 动态调整该客户端连接对应的文件描述符在 epoll 实例中所关注的事件类型
void WebServer::onProcess(HttpConn* client)
{
    // http成功生成响应
    if(client->process()) {
        // 监听可写事件，以便在写缓冲区有空间时通知服务器将生成的 HTTP 响应发送给客户端
        epoller_->modFd(client->getFd(), connEvent_ | EPOLLOUT);
    } else {
        // 该文件描述符的读缓冲区有数据可读，监听可读事件
        epoller_->modFd(client->getFd(), connEvent_ | EPOLLIN);
    }
}

// 处理客户端的读事件
void WebServer::onRead(HttpConn* client)
{
    assert(client);
    int ret = -1;
    int readErrno = 0;
    // 从连接客户端中读取数据
    ret = client->read(&readErrno);
    // 读取错误或客户端关闭连接
    if(ret <= 0 && readErrno != EAGAIN) {
        closeConn(client);
        return;
    }
    // 读取数据成功
    onProcess(client);
}

// 处理客户端的写事件
void WebServer::onWrite(HttpConn* client)
{
    assert(client);
    int ret = -1;
    int writeErrno = 0;
    // 向连接客户端中写入数据
    ret = client->write(&writeErrno);
    // 数据全部发送完成
    if(client->toWriteBytes() == 0) {
        if(client->isKeepAlive()) {
            onProcess(client);
            return;
        }
    } else if(ret < 0) {
        // 暂时的写缓冲区满问题，继续关注写事件以完成数据传输
        if(writeErrno == EAGAIN) {
            epoller_->modFd(client->getFd(), connEvent_ | EPOLLOUT);
            return;
        }
    }
    // 发生了严重错误的情况
    closeConn(client);
}

// 初始化服务器监听套接字
bool WebServer::initSocket()
{
    int ret = 0;
    struct sockaddr_in addr;
    // 端口号不合法
    if(port_ > 65535 || port_ < 1024) {
        LOG_ERROR("Port:%d error!",  port_);
        return false;
    }

    // 初始化服务器地址结构体
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(port_);

    // 配置套接字选项 struct linger 用于配置 SO_LINGER 选项
    // 该选项控制套接字关闭时的行为
    struct linger optLinger = {0};
    // 开启优雅关闭功能
    if(openLinger_) {
        // l_onoff 成员设置为 1，意味着开启 SO_LINGER 选项。当 SO_LINGER 选项开启时，套接字
        // 在关闭时会采用不同的行为模式
        optLinger.l_onoff = 1;
        // 当套接字关闭时，如果发送缓冲区中还有未发送完的数据，会尝试在 1 秒内将这些数据发送出去
        // 1 秒内数据没有发送成功，会被全部丢弃
        optLinger.l_linger = 1;
    }

    listenFd_ = socket(AF_INET, SOCK_STREAM, 0);
    if(listenFd_ < 0) {
        LOG_ERROR("Create socket error!", port_);
        return false;
    }

    // 为监听套接字配置 SO_LINGER 选项
    ret = setsockopt(listenFd_, SOL_SOCKET, SO_LINGER,
                    &optLinger, sizeof(optLinger));
    if(ret < 0) {
        close(listenFd_);
        LOG_ERROR("Init linger error!", port_);
        return false;
    }

    // 设置端口复用，只有最后一个套接字可以接收数据
    int optval = 1;
    // 在 SOL_SOCKET 层下，SO_REUSEADDR 用于设置端口复用
    ret = setsockopt(listenFd_, SOL_SOCKET, SO_REUSEADDR, 
                    (const void*)&optval, sizeof(int));
    if(ret == -1) {
        LOG_ERROR("Set socket setsockopt error!");
        close(listenFd_);
        return false;
    }

    // 绑定服务器并进行监听
    ret = bind(listenFd_, (struct sockaddr*)&addr, sizeof(addr));
    if(ret < 0) {
        LOG_ERROR("Bind Port : %d error!", port_);
        close(listenFd_);
        return false;
    }

    ret = listen(listenFd_, 6);
    if(ret < 0) {
        LOG_ERROR("Listen port : %d error!", port_);
        close(listenFd_);
        return false;
    }
    ret = epoller_->addFd(listenFd_, listenEvent_ | EPOLLIN);
    if(ret == 0) {
        LOG_ERROR("Add listen error!");
        close(listenFd_);
        return false;
    }
    setFdNonBlock(listenFd_);
    LOG_INFO("Server port : %d", port_);
    return true;
}

// 设置非阻塞模式
int WebServer::setFdNonBlock(int fd)
{
    assert(fd > 0);
    // F_GETFD 是获取文件的标志位，并且F_GETFD 命令不需要额外的参数
    return fcntl(fd, F_SETFL, fcntl(fd, F_GETFD, 0) | O_NONBLOCK);
}